package it.tukano.treditor.modules;

import it.tukano.app.AbstractAppModule;
import it.tukano.app.AppModule;
import it.tukano.treditor.engine.Engine3D;
import it.tukano.treditor.engine.EngineQueryInterface;
import it.tukano.treditor.engine.input.EdtEngineInputListener;
import it.tukano.treditor.engine.input.EngineInputListener;
import it.tukano.treditor.engine.scene.DirectionalLightElement;
import it.tukano.treditor.engine.scene.MeshElement;
import it.tukano.treditor.engine.scene.ModelElement;
import it.tukano.treditor.engine.scene.NodeElement;
import it.tukano.treditor.engine.scene.SceneElement;
import it.tukano.treditor.engine.scene.SceneView;
import it.tukano.treditor.modules.datamodel.DataModelEvent;
import it.tukano.treditor.modules.datamodel.DataModelListener;
import it.tukano.treditor.resources.Labels;
import it.tukano.treditor.resources.Labels.LabelKey;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.URL;
import java.util.ArrayList;
import javax.swing.JComponent;

/**
 * This module manages the 3d engine and its display
 * @author pgi
 */
public class EngineManager extends AbstractAppModule implements DataModelListener {
    
    /* The engine class */
    private final Class<? extends Engine3D> engineClass;
    private final Engine3D engine;
    private final ArrayList<EngineInputListener> listeners = new ArrayList<>();
    
    /**
     * Initializes this instance.
     * @param engine3dClassName fully qualified class name of the Engine3D class type
     * to load and instantiate.
     * @throws ClassNotFoundException if the class cannot be found
     * @throws InstantiationException if the class cannot be instantiated
     * @throws IllegalAccessException if the class has no public constructor
     */
    @SuppressWarnings("unchecked")
    public EngineManager(String engine3dClassName) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        this((Class<? extends Engine3D>) Class.forName(engine3dClassName));
    }

    /**
     * Initializes this istance. Eg. new EngineManager(JME3Engine.class)
     * @param engineClass the class of the Engine3D that will be used by this module
     * @throws InstantiationException if newInstance fails on the given class
     * @throws IllegalAccessException  if newInstance fails on the given class
     */
    public EngineManager(Class<? extends Engine3D> engineClass) throws InstantiationException, IllegalAccessException {
        this.engineClass = engineClass;
        this.engine = engineClass.newInstance();
    }

    /**
     * Initializes the engine after the initialization of this module by the context
     */
    @Override
    protected void postInitialize() {
        engine.edtInitialize();
    }

    /**
     * Starts the module. Get the engine display and installs it into the MainFrame
     * module
     * @param context the application's context
     */
    @Override
    public void startModule(AppModule context) {
        JComponent component = engine.edtGetDisplayComponent();
        component.setName(Labels.getInstance().getLabelValue(LabelKey.ENGINE_DISPLAY_NAME));
        find(MainFrame.class).install(component, BorderLayout.CENTER, false);
        find(DataModel.class).addListener(this);
        engine.setEngineInputListener(new EdtEngineInputListener(listeners));
    }

    /**
     * Destroy the engine
     * @param context the application's context
     */
    @Override
    public void stopModule(AppModule context) {
        super.stopModule(context);
        engine.edtDispose();
    }

    /**
     * Handles events sent by through the model
     * @param e the event to handle
     */
    public void dataModelEventPerformed(DataModelEvent e) {
        if(e.getEventSource() == this) {
            return;
        }
        
        switch(e.getType()) {
            case SCENE_ROOT_SET:
                handleSceneRootSet(e);
                break;
            case TEMP_ROOT_SET:
                handlTempRootSet(e);
                break;
            case NODE_ADD_MODEL:
                handleNodeAddModel(e);
                break;
            case NODE_ADD_EMPTY_NODE:
                handleNodeAddEmptyNode(e);
                break;
            case NODE_ADD_DIRECTIONAL_LIGHT:
                handleNodeAddDirectionalLight(e);
                break;
            case CURRENT_VIEW_SET:
                handleCurrentViewSet(e.getArg(0, SceneView.class));
                break;
            case MODEL_TRANSFORM_UPDATE:
                handleModelTransformUpdate(e.getArg(0, ModelElement.class));
                break;
            case DIRECTIONAL_LIGHT_UPDATE:
                handleDirectionalLightUpdate(e.getArg(0, DirectionalLightElement.class));
                break;
            case ELEMENT_REMOVED_FROM_NODE:
                handleElementRemovedFromNode(e.getArg(0, SceneElement.class), e.getArg(1, NodeElement.class));
                break;
            case NODE_ADD_MESH:
                handleNodeAddMesh(e);
                break;
        }
    }

    /**
     * The scene root has been set. Replace the engine root node with the new one
     * @param e the event
     */
    private void handleSceneRootSet(DataModelEvent e) {
        final NodeElement newRoot = e.getArg(0, NodeElement.class);
        engine.setSceneRoot(newRoot);
    }

    /**
     * The temporary root has been set. Replace the engine temporary root with the
     * new one
     * @param e the event to handle
     */
    private void handlTempRootSet(DataModelEvent e) {
        final NodeElement newTempRoot = e.getArg(0, NodeElement.class);
        engine.setTempRoot(newTempRoot);
    }

    /**
     * An element has been added to the scene
     * @param e the event
     */
    private void handleNodeAddModel(DataModelEvent e) {
        final NodeElement parent = e.getArg(0, NodeElement.class);
        final ModelElement child = e.getArg(1, ModelElement.class);
        engine.addModelToNode(child, parent);
    }

    private void handleNodeAddEmptyNode(DataModelEvent e) {
        final NodeElement parent = e.getArg(0, NodeElement.class);
        final NodeElement emptyChild = e.getArg(1, NodeElement.class);
        engine.addEmptyNodeToNode(emptyChild, parent);
    }

    private void handleNodeAddDirectionalLight(DataModelEvent e) {
        final NodeElement parentElement = e.getArg(0, NodeElement.class);
        final DirectionalLightElement lightElement = e.getArg(1, DirectionalLightElement.class);
        engine.addDirectionalLightToNode(lightElement, parentElement);
    }

    private void handleCurrentViewSet(SceneView view) {
        engine.setView(view);
    }

    private void handleModelTransformUpdate(ModelElement e) {
        engine.updateModelTransform(e);
    }

    private void handleDirectionalLightUpdate(DirectionalLightElement updatedLight) {
        engine.updateDirectionalLight(updatedLight);
    }

    public String[] getSupportedModelFormatExtensions() {
        return engine.getSupportedModelFormatExtensions();
    }

    public boolean isModelFormatSupported(URL modelUrl) {
        return engine.isModelFormatSupported(modelUrl);
    }
    
    /**
     * Add an input listener to this engine 
     * @param listener the listener to add
     */
    public void addEngineInputListener(EngineInputListener listener) {
        this.listeners.add(listener);
    }
    
    /**
     * Removes an input listener from the engine
     * @param listener the listener to add
     */
    public void removeEngineInputListener(EngineInputListener listener) {
        this.listeners.remove(listener);
    }

    private void handleElementRemovedFromNode(SceneElement element, NodeElement node) {
        engine.removeElementFromNode(element, node);
    }

    private void handleNodeAddMesh(DataModelEvent e) {
        final NodeElement parent = e.getArg(0, NodeElement.class);
        final MeshElement mesh = e.getArg(1, MeshElement.class);
        engine.addMeshToNode(mesh, parent);
    }

    public EngineQueryInterface getEngineQueryInterface() {
        return engine.getQueryInterface();
    }

    public Component getEngineDisplay() {
        return engine.edtGetDisplayComponent();
    }
}
